import tkinter as tk
from tkinter import ttk, messagebox, Menu
import datetime
import json
import os
import threading
import time
import winsound
from tkinter import font as tkfont
from tkcalendar import DateEntry
import pystray
from PIL import Image, ImageDraw
import requests
import pyperclip
import re
import sys
import calendar # Added for month calculations
import webbrowser # Added for opening registration link
import winreg # 用于检查管理员权限
import ctypes # 用于检查管理员权限和重启程序
import win32com.client # 用于创建快捷方式
import sys # 用于获取程序路径

# Requires pip install requests
# Requires pip install pyperclip
# Requires pip install pystray Pillow tkcalendar
# Requires pip install pywin32

from workday_checker import WorkdayChecker # Added import


class SettingsDialog(tk.Toplevel):
    def __init__(self, parent, app_instance):
        super().__init__(parent)
        self.transient(parent)
        self.grab_set()
        self.app = app_instance
        self.title("配置API Key, 模型和主题")
        self.geometry("450x300") # 增加高度以适应新选项
        self.resizable(False, False)
        self.attributes('-topmost', True)

        self.api_key_var = tk.StringVar(value=self.app.config.get("api_key", ""))
        self.ai_model_var = tk.StringVar(value=self.app.config.get("ai_model", "THUDM/GLM-4-9B-0414"))
        self.current_theme_var = tk.StringVar(value=self.app.config.get("theme", "clam"))

        main_frame = ttk.Frame(self, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # API Key Row
        ttk.Label(main_frame, text="硅基流动 APIKey:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        api_key_frame = ttk.Frame(main_frame)
        api_key_frame.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW)
        
        api_key_entry = ttk.Entry(api_key_frame, textvariable=self.api_key_var)
        api_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        
        register_button = ttk.Button(api_key_frame, text="免费注册", command=self.open_registration_link, style="Info.TButton")
        register_button.pack(side=tk.LEFT)

        # AI Model Row
        ttk.Label(main_frame, text="AI 模型名称:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
        ai_model_frame = ttk.Frame(main_frame)
        ai_model_frame.grid(row=1, column=1, padx=5, pady=5, sticky=tk.EW)
        
        ai_model_entry = ttk.Entry(ai_model_frame, textvariable=self.ai_model_var)
        ai_model_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        
        default_button = ttk.Button(ai_model_frame, text="默认", command=self.set_default_model, style="Secondary.TButton")
        default_button.pack(side=tk.LEFT)

        # Theme Row
        ttk.Label(main_frame, text="界面主题:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
        self.theme_combo = ttk.Combobox(main_frame, textvariable=self.current_theme_var, state="readonly", width=38)
        try:
            self.theme_combo['values'] = sorted(self.app.style.theme_names())
        except tk.TclError:
            self.theme_combo['values'] = ["clam", "alt", "default", "classic"]
        self.theme_combo.grid(row=2, column=1, padx=5, pady=5, sticky=tk.EW)
        
        # Info Label Row
        info_label = ttk.Label(main_frame, text="更改主题后可能需要重启应用以使所有元素生效。", wraplength=400, font=("TkDefaultFont", 8))
        info_label.grid(row=3, column=0, columnspan=2, padx=5, pady=(10,5), sticky=tk.W)

        # Button Frame Row
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=4, column=0, columnspan=2, pady=(10,0), sticky=tk.E)

        save_button = ttk.Button(button_frame, text="保存", command=self.save_settings, style="Success.TButton")
        save_button.pack(side=tk.LEFT, padx=5)

        cancel_button = ttk.Button(button_frame, text="取消", command=self.destroy, style="Secondary.TButton")
        cancel_button.pack(side=tk.LEFT, padx=5)

        main_frame.grid_columnconfigure(1, weight=1)

    def open_registration_link(self):
        try:
            webbrowser.open_new_tab("https://cloud.siliconflow.cn/i/PcAl8EaR")
        except Exception as e:
            messagebox.showerror("打开链接失败", f"无法打开注册链接: {e}", parent=self)
            
    def set_default_model(self):
        self.ai_model_var.set("THUDM/GLM-4-9B-0414")
        messagebox.showinfo("已设置默认模型", "已将AI模型设置为默认值: THUDM/GLM-4-9B-0414", parent=self)

    def save_settings(self):
        new_api_key = self.api_key_var.get().strip()
        new_ai_model = self.ai_model_var.get().strip()
        if not new_ai_model: # If user clears the field, reset to default
            new_ai_model = "THUDM/GLM-4-9B-0414"
            self.ai_model_var.set(new_ai_model) # Update UI in dialog

        new_theme = self.current_theme_var.get()

        self.app.config["api_key"] = new_api_key
        self.app.config["ai_model"] = new_ai_model
        
        theme_changed = False
        if self.app.config.get("theme") != new_theme:
            self.app.config["theme"] = new_theme
            theme_changed = True

        self.app.save_settings()

        if theme_changed:
            try:
                self.app.style.theme_use(new_theme)
                self.app.configure_styles() 
                messagebox.showinfo("主题已更改", f"主题已更改为 '{new_theme}'. 部分更改可能需要重启应用。", parent=self)
            except tk.TclError:
                messagebox.showerror("主题错误", f"无法应用主题 '{new_theme}'.", parent=self)
        
        messagebox.showinfo("设置已保存", "API Key, AI 模型和主题设置已保存。", parent=self)
        self.destroy()


class TodoReminderApp:
    def __init__(self, root, start_minimized=False):
        self.root = root
        self.root.title("待办提醒")
        self.root.geometry("800x750") 
        self.root.resizable(True, True)
        
        # 检查是否以管理员权限运行
        self.is_admin = self.check_admin()
        
        # 如果需要管理员权限且当前不是管理员，提示用户
        if not self.is_admin and "--admin-required" in sys.argv:
            self.root.after(500, self.show_admin_required_dialog)

        if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
            self.application_path = os.path.dirname(sys.executable)
        else:
            self.application_path = os.path.dirname(os.path.abspath(__file__))

        self.settings_file = os.path.join(self.application_path, "settings.json")
        self.data_file = os.path.join(self.application_path, "reminders.json")

        self.config = {}
        self.load_settings()

        self.style = ttk.Style(self.root)
        self.apply_saved_theme() 
    
        # 检查API Key是否已配置
        self.check_api_key_configured()

        self.configure_styles()
        self.create_menu()

        self.setup_tray()
        self.default_font = tkfont.nametofont("TkDefaultFont")
        self.default_font.configure(size=10)
        
        self.workday_checker = WorkdayChecker(cache_dir=os.path.join(self.application_path, "workday_cache"))

        # Define recurrence_type display and internal value mapping
        self.recurrence_type_map = {
            "once": "一次性",
            "daily": "每天",
            "workday": "工作日",
            "weekend": "休息日",
            "weekly": "每周",
            "monthly": "每月"
        }
        self.recurrence_type_display_to_value = {v: k for k, v in self.recurrence_type_map.items()}

        self.reminders = []
        self.load_reminders()
        self.create_ui() 

        self.stop_thread = False
        self.reminder_thread = threading.Thread(target=self.check_reminders)
        self.reminder_thread.daemon = True
        self.reminder_thread.start()

        self.clipboard_thread = threading.Thread(target=self.monitor_clipboard)
        self.clipboard_thread.daemon = True
        self.clipboard_thread.start()

        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.root.bind("<Unmap>", self.on_minimize)
        self.last_clipboard_content = ""

        self.keywords = [
            # 中文常见提醒关键词
            "会议", "提醒", "待办", "任务", "预约", "日程", "分享", "培训", "完成",
            "活动", "安排", "讲座", "讨论", "截止", "通知", "备忘", "记得", "别忘", 
            "开会", "汇报", "复盘", "面试", "签到", "签约", "交付", "提交", "检查",
            "复习", "考试", "考核", "打卡", "回复", "联系", "拜访", "探望", "取件",
            # 英文常见提醒关键词
            "ddl", "due", "meeting", "reminder", "todo", "appointment", "schedule", "deadline",
            "call", "meet", "check", "review", "submit", "send", "reply", "contact"
        ]
        self.datetime_patterns = [
            # 标准日期时间格式
            r'\d{4}[-/年]\s*\d{1,2}[-/月]\s*\d{1,2}[日号]?\s*(?:上午|下午)?\s*\d{1,2}[:：]\d{1,2}(?:[:：]\d{1,2})?',
            r'\d{1,2}[-/月]\s*\d{1,2}[日号]?\s*(?:上午|下午)?\s*\d{1,2}[:：]\d{1,2}(?:[:：]\d{1,2})?',
            # 相对日期（今天/明天/后天）
            r'(?:今天|明天|后天|今日|明日|後日)\s*(?:上午|中午|下午|晚上)?\s*\d{1,2}[:：]\d{1,2}(?:[:：]\d{1,2})?',
            r'(?:今天|明天|后天|今日|明日|後日)\s*(?:上午|中午|下午|晚上)?\s*\d{1,2}\s*[点时](?:半|整|[\d零一二三四五六七八九十]{1,2}分)?',
            # 时间表达式
            r'(?:上午|中午|下午|晚上)\s*\d{1,2}\s*[点时](?:半|整|[\d零一二三四五六七八九十]{1,2}分)?',
            r'\d{1,2}\s*[点时](?:半|整|[\d零一二三四五六七八九十]{1,2}分)?',
            # 星期表达式
            r'(?:周|星期)[一二三四五六日天]\s*(?:上午|中午|下午|晚上)?\s*\d{1,2}[:：]\d{1,2}',
            r'(?:周|星期)[一二三四五六日天]\s*(?:上午|中午|下午|晚上)?\s*\d{1,2}\s*[点时](?:半|整|[\d零一二三四五六七八九十]{1,2}分)?',
            # 模糊时间表达式
            r'(?:下|这|本|下下)\s*(?:周|星期|礼拜)[一二三四五六日天]',
            r'(?:下|这|本)\s*(?:个)?\s*月\s*\d{1,2}\s*[日号]',
            # 英文日期时间格式
            r'\d{1,2}\s*:\s*\d{1,2}\s*(?:am|pm|AM|PM)',
            r'(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s*\d{1,2}(?:st|nd|rd|th)?',
            r'(?:tomorrow|today|next week)\s*(?:at)?\s*\d{1,2}\s*(?::\s*\d{1,2})?\s*(?:am|pm|AM|PM)?'
        ]
        
        if start_minimized:
            self.root.withdraw()

    @staticmethod
    def resource_path(relative_path):
        try:
            base_path = sys._MEIPASS
        except AttributeError: 
            base_path = os.path.abspath(".") 
        return os.path.join(base_path, relative_path)

    def load_settings(self):
        default_settings = {
            "api_key": "", 
            "theme": "clam",
            "ai_model": "THUDM/GLM-4-9B-0414", # Added default AI model
            "autostart": False # 默认不开机启动
        }
        if os.path.exists(self.settings_file):
            try:
                with open(self.settings_file, "r", encoding="utf-8") as f:
                    self.config = json.load(f)
                for key, value in default_settings.items():
                    if key not in self.config:
                        self.config[key] = value
            except json.JSONDecodeError:
                self.config = default_settings
                self.save_settings() 
            except Exception as e:
                self.config = default_settings
        else:
            self.config = default_settings
            self.save_settings()

    def save_settings(self):
        try:
            with open(self.settings_file, "w", encoding="utf-8") as f:
                json.dump(self.config, f, ensure_ascii=False, indent=2)
        except Exception as e:
            if self.root.winfo_exists():
                messagebox.showerror("设置保存错误", f"无法保存设置: {e}", parent=self.root)

    def apply_saved_theme(self):
        theme_to_apply = self.config.get("theme", "clam")
        available_themes = self.style.theme_names()
        if theme_to_apply in available_themes:
            try:
                self.style.theme_use(theme_to_apply)
            except tk.TclError as e:
                self.try_fallback_theme()
        else:
            self.try_fallback_theme()
            if theme_to_apply != self.config.get("theme"): # if fallback changed the theme in config
                self.save_settings()

    def try_fallback_theme(self):
        fallback_themes = ['clam', 'alt', 'default', 'vista', 'xpnative'] 
        current_theme_applied = self.style.theme_use() # Get current theme before trying fallbacks
        
        for theme in fallback_themes:
            if theme in self.style.theme_names():
                try:
                    self.style.theme_use(theme)
                    self.config["theme"] = theme # Update config with successful fallback
                    return
                except tk.TclError:
                    continue
        # If all fallbacks fail, ensure config reflects the theme that was actually applied (or originally was)
        self.config["theme"] = current_theme_applied


    def create_menu(self):
        self.menu_bar = Menu(self.root)
        self.root.config(menu=self.menu_bar)
        settings_menu = Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="设置", menu=settings_menu)
        settings_menu.add_command(label="配置...", command=self.open_settings_dialog)
        
        # 创建开机启动变量并设置初始值
        self.autostart_var = tk.BooleanVar(value=self.config.get("autostart", False))
        
        settings_menu.add_separator()
        settings_menu.add_checkbutton(label="开机启动", variable=self.autostart_var, command=lambda: self.toggle_autostart())
        
    def toggle_autostart(self):
        new_state = self.autostart_var.get()
        success = self.set_autostart(new_state)
        if success:
            self.config["autostart"] = new_state
            self.save_settings()
        else:
            # 如果设置失败，恢复变量状态
            self.autostart_var.set(not new_state)

    def open_settings_dialog(self):
        SettingsDialog(self.root, self)
        
    # 检查是否具有管理员权限
    def check_admin(self):
        try:
            return ctypes.windll.shell32.IsUserAnAdmin() != 0
        except:
            return False
            
    # 显示需要管理员权限的对话框
    def show_admin_required_dialog(self):
        result = messagebox.askokcancel(
            "需要管理员权限",
            "设置开机启动需要管理员权限。\n\n是否关闭当前程序并以管理员身份重新启动？",
            parent=self.root
        )
        if result:
            # 准备以管理员身份重启程序
            if getattr(sys, 'frozen', False):
                # 如果是打包后的exe
                exe_path = sys.executable
            else:
                # 如果是Python脚本
                exe_path = sys.argv[0]
                
            # 构建命令行参数，保留原有参数但移除admin-required标记
            args = [arg for arg in sys.argv[1:] if arg != "--admin-required"]
            
            try:
                # 使用ShellExecute以管理员身份运行程序
                ctypes.windll.shell32.ShellExecuteW(
                    None,  # 父窗口句柄
                    "runas",  # 操作：以管理员身份运行
                    exe_path,  # 可执行文件路径
                    " ".join(args),  # 命令行参数
                    os.path.dirname(exe_path),  # 工作目录
                    1  # 显示窗口
                )
                # 关闭当前程序
                self.quit_app()
            except Exception as e:
                messagebox.showerror("启动失败", f"以管理员身份启动程序失败: {e}", parent=self.root)
            
    # 设置开机启动
    def set_autostart(self, enable=None):
        if enable is None:
            enable = self.config.get("autostart", False)
        
        # 获取当前可执行文件路径
        if getattr(sys, 'frozen', False):
            # 如果是打包后的exe
            exe_path = sys.executable
        else:
            # 如果是Python脚本
            exe_path = sys.argv[0]
            
        # 获取启动文件夹路径
        startup_folder = os.path.join(os.environ["APPDATA"], "Microsoft", "Windows", "Start Menu", "Programs", "Startup")
        shortcut_path = os.path.join(startup_folder, "待办提醒.lnk")
        
        # 尝试使用普通用户权限操作
        try:
            if enable:
                # 创建快捷方式
                try:
                    shell = win32com.client.Dispatch("WScript.Shell")
                    shortcut = shell.CreateShortCut(shortcut_path)
                    shortcut.TargetPath = exe_path
                    shortcut.Arguments = "--startup" # 添加启动参数
                    shortcut.WorkingDirectory = os.path.dirname(exe_path)
                    shortcut.IconLocation = exe_path + ",0"
                    shortcut.save()
                    return True
                except Exception as e:
                    # 如果失败且不是管理员，可能需要管理员权限
                    if not self.is_admin:
                        result = messagebox.askokcancel(
                            "设置开机启动失败",
                            f"创建快捷方式失败: {e}\n\n可能需要管理员权限。是否以管理员身份重新启动程序？",
                            parent=self.root
                        )
                        if result:
                            # 添加管理员标记并重启
                            args = sys.argv.copy()
                            if "--admin-required" not in args:
                                args.append("--admin-required")
                                
                            # 使用ShellExecute以管理员身份运行程序
                            ctypes.windll.shell32.ShellExecuteW(
                                None, "runas", exe_path, " ".join(args[1:]),
                                os.path.dirname(exe_path), 1
                            )
                            # 关闭当前程序
                            self.quit_app()
                            return False
                    else:
                        messagebox.showerror("设置开机启动失败", f"创建快捷方式失败: {e}", parent=self.root)
                    return False
            else:
                # 删除快捷方式
                if os.path.exists(shortcut_path):
                    os.remove(shortcut_path)
                return True
        except Exception as e:
            # 如果失败且不是管理员，可能需要管理员权限
            if not self.is_admin:
                result = messagebox.askokcancel(
                    "操作失败",
                    f"操作失败: {e}\n\n可能需要管理员权限。是否以管理员身份重新启动程序？",
                    parent=self.root
                )
                if result:
                    # 添加管理员标记并重启
                    args = sys.argv.copy()
                    if "--admin-required" not in args:
                        args.append("--admin-required")
                        
                    # 使用ShellExecute以管理员身份运行程序
                    ctypes.windll.shell32.ShellExecuteW(
                        None, "runas", exe_path, " ".join(args[1:]),
                        os.path.dirname(exe_path), 1
                    )
                    # 关闭当前程序
                    self.quit_app()
                    return False
            else:
                messagebox.showerror("操作失败", f"操作失败: {e}", parent=self.root)
            return False

    def check_api_key_configured(self):
        """
        检查API Key是否已配置，如果未配置则提示用户并打开设置窗口
        """
        API_KEY = self.config.get("api_key", "")
        if not API_KEY or API_KEY == "YOUR_SILICONFLOW_API_KEY_HERE":
            if self.root.winfo_exists():
                result = messagebox.askokcancel(
                    "API Key未配置", 
                    "您尚未配置硅基流动API Key，无法使用AI分析功能。\n\n是否现在配置？",
                    parent=self.root
                )
                if result:
                    self.root.after(100, self.open_settings_dialog)  # 使用after确保消息框关闭后再打开设置窗口
                    
    def configure_styles(self):
        self.style.configure("Success.TButton", foreground="black", background="#4CAF50", padding=3)
        self.style.map("Success.TButton", background=[('active', '#45a049')])
        self.style.configure("Edit.TButton", foreground="black", background="#FFC107", padding=3)
        self.style.map("Edit.TButton", background=[('active', '#ffb300')])
        self.style.configure("Danger.TButton", foreground="white", background="#F44336", padding=3)
        self.style.map("Danger.TButton", background=[('active', '#e53935')])
        self.style.configure("Info.TButton", foreground="black", background="#2196F3", padding=3)
        self.style.map("Info.TButton", background=[('active', '#1e88e5')])
        self.style.configure("Secondary.TButton", foreground="black", background="#E0E0E0", padding=3)
        self.style.map("Secondary.TButton", background=[('active', '#bdbdbd')])
        self.style.configure("Recurrence.TFrame", padding=0)
        
    def setup_tray(self):
        try:
            icon_image = self.create_tray_icon()
            menu = (pystray.MenuItem("显示主窗口", self.show_window, default=True),
                    pystray.MenuItem("退出", self.quit_app))
            self.icon = pystray.Icon("todo_reminder", icon_image, "待办提醒", menu)
            threading.Thread(target=self.icon.run, daemon=True).start()
        except Exception as e:
            print(f"Failed to setup tray icon: {e}") # Log error
            self.icon = None

    def create_tray_icon(self):
        width, height, color1, color2 = 64, 64, "black", "white"
        image = Image.new('RGB', (width, height), color2)
        dc = ImageDraw.Draw(image)
        dc.ellipse((8, 8, width-8, height-8), outline=color1, width=4)
        dc.line((width//2, height//2, width//2, 16), fill=color1, width=4)
        dc.line((width//2, height//2, width-16, height//2), fill=color1, width=4)
        return image

    def show_window(self):
        self.root.after(0, self._show_window_thread_safe)

    def _show_window_thread_safe(self):
        if not self.root.winfo_exists(): return
        self.root.deiconify()
        self.root.state('normal')
        self.root.lift()
        self.root.focus_force()
        self.root.attributes('-topmost', True)
        if self.root.winfo_exists(): # Re-check existence for delayed lambda
            self.root.after(100, lambda: self.root.attributes('-topmost', False) if self.root.winfo_exists() else None)


    def on_minimize(self, event):
        if event.widget == self.root and self.root.state() == 'iconic':
            self.root.withdraw()
            
    def set_today(self):
        self.date_entry.set_date(datetime.datetime.now().date())
        
    def set_tomorrow(self):
        tomorrow = datetime.datetime.now().date() + datetime.timedelta(days=1)
        self.date_entry.set_date(tomorrow)
        
    def set_minus_2min(self):
        try:
            current_time_str = self.time_entry.get().strip()
            if not re.match(r'^\d{2}:\d{2}$', current_time_str):
                if self.root.winfo_exists(): messagebox.showerror("时间错误", "时间格式应为 HH:MM", parent=self.root)
                return
            hour, minute = map(int, current_time_str.split(':'))
            current_dt = datetime.datetime.combine(self.date_entry.get_date(), datetime.time(hour=hour, minute=minute))
            new_dt = current_dt - datetime.timedelta(minutes=2)
            self.date_entry.set_date(new_dt.date()) 
            self.time_entry.delete(0, tk.END)
            self.time_entry.insert(0, new_dt.strftime("%H:%M"))
        except Exception as e:
            if self.root.winfo_exists(): messagebox.showerror("错误", f"设置时间时出错: {e}", parent=self.root)

    def quit_app(self):
        self.stop_thread = True
        if hasattr(self, 'icon') and self.icon: self.icon.stop()
        if hasattr(self, 'reminder_thread') and self.reminder_thread.is_alive(): self.reminder_thread.join(timeout=1)
        if hasattr(self, 'clipboard_thread') and self.clipboard_thread.is_alive(): self.clipboard_thread.join(timeout=1)
        if hasattr(self, 'reminders'): self.save_reminders()
        if self.root.winfo_exists():
            self.root.quit()
            self.root.destroy()
        else: # If root window already destroyed (e.g. by OS signal not WM_DELETE_WINDOW)
            sys.exit(0)


    def on_closing(self): self.root.withdraw()

    def analyze_text(self, text):
        API_KEY = self.config.get("api_key", "")
        AI_MODEL = self.config.get("ai_model", "THUDM/GLM-4-9B-0414") # Use configured model

        if not API_KEY or API_KEY == "YOUR_SILICONFLOW_API_KEY_HERE": # Placeholder check
            if self.root.winfo_exists():
                result = messagebox.askokcancel(
                    "API Key未配置", 
                    "您尚未配置硅基流动API Key，无法使用AI分析功能。\n\n是否现在配置？",
                    parent=self.root
                )
                if result:
                    self.root.after(100, self.open_settings_dialog)  # 使用after确保消息框关闭后再打开设置窗口
            return None
        try:
            url = "https://api.siliconflow.cn/v1/chat/completions"
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {API_KEY}"
            }
            now = datetime.datetime.now()
            nowDate = now.strftime("%Y-%m-%d")
            #当天星期几
            weekday = calendar.weekday(now.year, now.month, now.day)
            data = {
                "model": AI_MODEL, # Use the configured model name
                "messages": [
                    {
                        "role": "system",
                        "content": (
                            "你是一个专业的待办事项分析助手。你需要从文本中提取关键信息，并生成结构化的待办事项数据。"
                            "请确保返回的JSON是严格有效的，所有字符串和键名都使用双引号，并且对象或数组的最后一个元素后没有逗号。"
                            "如果文本是关于一个会议、活动或分享，请将主要事件作为 'title'，相关的详细信息（如地点、主讲人、会议号等）放入 'content'。"
                            "从文本中提取最明确的日期和时间作为 'datetime'。"
                            "对于日期处理，请遵循以下规则："
                            "1. 如果文本中明确包含完整年份，则使用该年份。"
                            "2. 如果文本中只有月份和日期（如'5月20日'），则根据当前日期智能判断年份："
                            "   - 如果该日期在当前日期之后（或当天），使用当前年份。"
                            "   - 如果该日期已经过去，则使用下一年的年份。"
                            "3. 对于'明天'、'后天'等相对日期，请基于当前日期计算具体日期。"
                            "4. 始终返回标准格式的日期时间：'YYYY-MM-DD HH:MM'。"
                        )
                    },
                    {
                        "role": "user",
                        "content": (
                            "我需要从给出的文本中提取json结构化数据,用于创建待办任务. "
                            "title为提醒主题 (例如：AI分享会). "
                            "content为需要的重要信息 (例如：地点:2601会议室, 主讲人:刘洁老师, 备注:可能转线上腾讯会议). "
                            "datetime为提醒时间 (例如：2025-05-06 14:30). "
                            "注意：如果文本中没有明确的年份信息，请根据以下规则处理："
                            f"- 当前日期为:{nowDate},今天是星期{['一','二','三','四','五','六','日'][weekday]}"
                            "- 如果日期在当前日期之后或当天，使用当前年份"
                            "- 如果日期已过去，则使用下一年的年份"
                            "- 对于相对日期（如明天、后天），基于当前日期计算"
                            "- 始终返回标准格式：YYYY-MM-DD HH:MM"
                            "回答不要给出其他额外内容.只需要json数据如:\n"
                            "{\n"
                            "  \"title\": \"项目研讨会\",\n"
                            "  \"content\": \"会议号:524512 会议密码:52365\",\n"
                            "  \"datetime\": \"2025-05-07 16:44\"\n"
                            "}\n"
                            f"现在需要提取的文本内容为:\n{text}"
                        )
                    }
                ],
                "temperature": 0.5,
                "max_tokens": 512
            }

            response = requests.post(url, headers=headers, json=data, timeout=30)
            response.raise_for_status()

            result = response.json()
            if not result.get('choices') or not result['choices'][0].get('message') or not result['choices'][0]['message'].get('content'):
                raise ValueError("AI响应格式不正确，缺少必要内容。")

            ai_raw_content = result['choices'][0]['message']['content']
            json_text_to_parse = ai_raw_content.strip()
            match_md_json = re.search(r'```json\s*(\{[\s\S]*?\})\s*```', json_text_to_parse, re.DOTALL | re.IGNORECASE)
            if match_md_json:
                json_text_to_parse = match_md_json.group(1)
            else:
                first_brace = json_text_to_parse.find('{')
                last_brace = json_text_to_parse.rfind('}')
                if first_brace != -1 and last_brace != -1 and last_brace > first_brace:
                    json_text_to_parse = json_text_to_parse[first_brace : last_brace + 1]

            try:
                todo_data = json.loads(json_text_to_parse)
            except json.JSONDecodeError as e:
                # Attempt to fix common JSON issues like trailing commas or comments
                fixed_json_text = re.sub(r',\s*([}\]])', r'\1', json_text_to_parse) # Remove trailing commas
                fixed_json_text = re.sub(r'//.*', '', fixed_json_text) # Remove // comments
                # Remove /* ... */ comments, non-greedy
                fixed_json_text = re.sub(r'/\*[\s\S]*?\*/', '', fixed_json_text, flags=re.MULTILINE).strip()
                try:
                    todo_data = json.loads(fixed_json_text)
                except json.JSONDecodeError as e2:
                    # Attempt to fix single quotes and Pythonic None/True/False
                    text_for_eval = fixed_json_text.replace('None', 'null').replace('True', 'true').replace('False', 'false')
                    text_for_eval = text_for_eval.replace("'", '"') # Risky, but sometimes helps
                    try:
                        todo_data = json.loads(text_for_eval)
                    except json.JSONDecodeError as e3:
                        raise ValueError(f"无法从AI响应中解析JSON。原始: {e}, 修复1: {e2}, 修复2: {e3}") from e3

            if not isinstance(todo_data, dict):
                raise ValueError(f"解析后的数据不是一个字典: {type(todo_data)}")
            return todo_data

        except requests.exceptions.Timeout:
            if self.root.winfo_exists(): messagebox.showerror("错误", "AI API请求超时，请检查网络或稍后再试。", parent=self.root)
        except requests.exceptions.RequestException as e_req:
            if self.root.winfo_exists(): messagebox.showerror("错误", f"AI API请求失败: {str(e_req)}", parent=self.root)
        except ValueError as e_val: # Includes JSON parsing errors
            if self.root.winfo_exists(): messagebox.showerror("错误", f"AI分析或JSON处理失败: {str(e_val)}", parent=self.root)
        except Exception as e: # Catch-all for other unexpected errors
            if self.root.winfo_exists(): messagebox.showerror("错误", f"AI分析时发生未知错误: {str(e)}", parent=self.root)
        return None

    def create_ui(self):
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        self.root.minsize(800, 750) 

        add_frame = ttk.LabelFrame(main_frame, text="添加/编辑提醒", padding="10")
        add_frame.pack(fill=tk.X, pady=5)
        add_frame.grid_columnconfigure(1, weight=1) 

        current_row = 0
        ai_frame = ttk.LabelFrame(add_frame, text="AI智能分析 (粘贴文本后点击分析)", padding="5")
        ai_frame.grid(row=current_row, column=0, columnspan=3, sticky=tk.EW, pady=(0,10)) 
        current_row += 1

        self.ai_text = tk.Text(ai_frame, width=50, height=4, wrap=tk.WORD, relief=tk.SOLID, borderwidth=1)
        self.ai_text.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,5))
        analyze_button = ttk.Button(ai_frame, text="分析", command=self.on_analyze, style="Info.TButton")
        analyze_button.pack(side=tk.RIGHT, padx=(5,0), ipady=2)

        ttk.Label(add_frame, text="提醒事项:").grid(row=current_row, column=0, sticky=tk.W, pady=2)
        self.reminder_text = ttk.Entry(add_frame, width=40)
        self.reminder_text.grid(row=current_row, column=1, columnspan=2, sticky=tk.EW, pady=2)
        current_row += 1

        ttk.Label(add_frame, text="提醒内容:").grid(row=current_row, column=0, sticky=tk.NW, pady=2)
        self.reminder_content = tk.Text(add_frame, width=40, height=3, wrap=tk.WORD, relief=tk.SOLID, borderwidth=1)
        self.reminder_content.grid(row=current_row, column=1, columnspan=2, sticky=tk.EW, pady=2)
        current_row += 1

        datetime_outer_frame = ttk.Frame(add_frame)
        datetime_outer_frame.grid(row=current_row, column=0, columnspan=3, sticky=tk.EW, pady=2)
        datetime_outer_frame.grid_columnconfigure(1, weight=1) # Allow date_entry to not be squashed

        ttk.Label(datetime_outer_frame, text="开始日期:").grid(row=0, column=0, sticky=tk.W, padx=(0,5))
        self.date_entry = DateEntry(datetime_outer_frame, width=12, background='darkblue', foreground='white',
                                    borderwidth=2, date_pattern='yyyy-mm-dd', firstweekday='monday')
        self.date_entry.grid(row=0, column=1, sticky=tk.W, padx=5)
        
        ttk.Label(datetime_outer_frame, text="时间:").grid(row=0, column=2, sticky=tk.W, padx=(10,5))
        self.time_entry = ttk.Entry(datetime_outer_frame, width=8)
        self.time_entry.grid(row=0, column=3, sticky=tk.W, padx=5)
        self.time_entry.insert(0, (datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime("%H:%M"))
        current_row += 1 # This row conceptually ends here for grid counting

        quick_btn_frame = ttk.Frame(datetime_outer_frame)
        quick_btn_frame.grid(row=0, column=4, sticky=tk.E, padx=(10,0)) # Place on same visual row as date/time
        
        today_btn = ttk.Button(quick_btn_frame, text="当天", width=6, command=self.set_today)
        today_btn.pack(side=tk.LEFT, padx=2)
        tomorrow_btn = ttk.Button(quick_btn_frame, text="明天", width=6, command=self.set_tomorrow)
        tomorrow_btn.pack(side=tk.LEFT, padx=2)
        minus_2min_btn = ttk.Button(quick_btn_frame, text="提前2分钟", width=10, command=self.set_minus_2min)
        minus_2min_btn.pack(side=tk.LEFT, padx=2)
        
        ttk.Label(add_frame, text="重复类型:").grid(row=current_row, column=0, sticky=tk.W, pady=2)
        self.recurrence_type_var = tk.StringVar(value=self.recurrence_type_map["once"]) # Default to Chinese display
        
        recurrence_display_values = list(self.recurrence_type_map.values()) # Get Chinese display values

        self.recurrence_combo = ttk.Combobox(add_frame, textvariable=self.recurrence_type_var,
                                             values=recurrence_display_values, state="readonly", width=15)
        self.recurrence_combo.grid(row=current_row, column=1, sticky=tk.W, pady=2)
        self.recurrence_combo.bind("<<ComboboxSelected>>", self.on_recurrence_type_change)

        self.recurrence_options_frame = ttk.Frame(add_frame, style="Recurrence.TFrame")
        self.recurrence_options_frame.grid(row=current_row, column=2, sticky=tk.EW, pady=0, padx=5)
        current_row += 1

        # --- Weekly options frame (child of recurrence_options_frame) ---
        self.weekly_options_frame = ttk.Frame(self.recurrence_options_frame, style="Recurrence.TFrame")
        self.weekly_days_vars = [tk.BooleanVar(value=False) for _ in range(7)]
        days_names = ["一", "二", "三", "四", "五", "六", "日"]
        for i, day_name in enumerate(days_names):
            cb = ttk.Checkbutton(self.weekly_options_frame, text=day_name, variable=self.weekly_days_vars[i], width=3)
            cb.pack(side=tk.LEFT, padx=1, pady=0) # Use padx=1 for tighter packing
        
        # --- Monthly options frame (child of recurrence_options_frame) ---
        self.monthly_option_frame = ttk.Frame(self.recurrence_options_frame, style="Recurrence.TFrame")
        ttk.Label(self.monthly_option_frame, text="每月:").pack(side=tk.LEFT, padx=(0,2), pady=0)
        self.monthly_day_var = tk.StringVar(value="1")
        self.monthly_day_spinbox = ttk.Spinbox(self.monthly_option_frame, from_=1, to=31, 
                                              textvariable=self.monthly_day_var, width=3, wrap=True)
        self.monthly_day_spinbox.pack(side=tk.LEFT, pady=0)
        ttk.Label(self.monthly_option_frame, text="号").pack(side=tk.LEFT, padx=(2,0), pady=0)

        self.on_recurrence_type_change() # Initialize visibility of recurrence options

        self.sound_var = tk.BooleanVar(value=True)
        sound_check_frame = ttk.Frame(add_frame)
        sound_check_frame.grid(row=current_row, column=1, columnspan=2, sticky=tk.W, pady=2)
        self.sound_check = ttk.Checkbutton(sound_check_frame, text="声音提醒", variable=self.sound_var)
        self.sound_check.pack(side=tk.LEFT)
        current_row += 1

        action_buttons_frame = ttk.Frame(add_frame)
        action_buttons_frame.grid(row=current_row, column=0, columnspan=3, sticky=tk.EW, pady=(10,5))

        common_btn_options = {"compound": tk.LEFT, "width": 10}
        add_button = ttk.Button(action_buttons_frame, text="添加", command=self.add_reminder, style="Success.TButton", **common_btn_options)
        add_button.pack(side=tk.LEFT, padx=3)
        edit_button = ttk.Button(action_buttons_frame, text="编辑", command=self.edit_selected, style="Edit.TButton", **common_btn_options)
        edit_button.pack(side=tk.LEFT, padx=3)
        delete_button = ttk.Button(action_buttons_frame, text="删除", command=self.delete_selected, style="Danger.TButton", **common_btn_options)
        delete_button.pack(side=tk.LEFT, padx=3)
        complete_button = ttk.Button(action_buttons_frame, text="完成此项", command=self.mark_as_completed, style="Info.TButton", **common_btn_options)
        complete_button.pack(side=tk.LEFT, padx=3)
        refresh_button = ttk.Button(action_buttons_frame, text="刷新", command=self.refresh_reminders, style="Secondary.TButton", **common_btn_options)
        refresh_button.pack(side=tk.RIGHT, padx=3) # Align refresh to the right

        list_frame = ttk.LabelFrame(main_frame, text="提醒列表", padding="10")
        list_frame.pack(fill=tk.BOTH, expand=True, pady=(5,0))

        # This container will hold the Treeview and its Scrollbar
        tree_view_area_container = ttk.Frame(list_frame)
        tree_view_area_container.pack(fill=tk.BOTH, expand=True) # This frame fills the list_frame

        columns = ("id", "title", "content", "datetime", "recurrence_display", "status")
        # self.tree is now a child of tree_view_area_container
        self.tree = ttk.Treeview(tree_view_area_container, columns=columns, show="headings", selectmode="extended")

        self.tree.heading("id", text="ID", command=lambda: self.sort_treeview_column("id", False))
        self.tree.heading("title", text="提醒事项", command=lambda: self.sort_treeview_column("title", False))
        self.tree.heading("content", text="提醒内容") # No sort for content by default
        self.tree.heading("datetime", text="下次时间", command=lambda: self.sort_treeview_column("datetime", False))
        self.tree.heading("recurrence_display", text="重复规则", command=lambda: self.sort_treeview_column("recurrence_display", False))
        self.tree.heading("status", text="状态", command=lambda: self.sort_treeview_column("status", False))

        self.tree.column("id", width=40, minwidth=30, stretch=tk.NO)
        self.tree.column("title", width=150, minwidth=100) 
        self.tree.column("content", width=230, minwidth=150) 
        self.tree.column("datetime", width=130, minwidth=100)
        self.tree.column("recurrence_display", width=100, minwidth=80)
        self.tree.column("status", width=70, minwidth=60, stretch=tk.NO)

        # Scrollbar is also a child of tree_view_area_container
        scrollbar_y = ttk.Scrollbar(tree_view_area_container, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar_y.set) # Use yscrollcommand

        # Pack order within tree_view_area_container is important:
        scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y) # Scrollbar to the right
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # Tree fills the rest

        self.tree.bind("<Double-1>", self.on_item_double_click)
        self.refresh_reminders()

    def on_recurrence_type_change(self, event=None):
        recurrence_type_display = self.recurrence_type_var.get() # Get Chinese display value
        # Convert display value to internal English value for logic
        recurrence_type_value = self.recurrence_type_display_to_value.get(recurrence_type_display, "once")
        
        self.weekly_options_frame.pack_forget()
        self.monthly_option_frame.pack_forget()

        if recurrence_type_value == "weekly":
            self.weekly_options_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
        elif recurrence_type_value == "monthly":
            self.monthly_option_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)

    def get_recurrence_display_text(self, reminder):
        recur_type_value = reminder.get("recurrence_type", "once") # internal English value
        display_text = self.recurrence_type_map.get(recur_type_value, recur_type_value) # Get Chinese display

        if recur_type_value == "weekly":
            days_indices = reminder.get("recurrence_days_of_week", []) # Should be list of indices
            if not days_indices: return f"{display_text} (未选)"
            days_names_map = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
            selected_days_text = [days_names_map[i] for i in days_indices if 0 <= i < 7]
            return f"{display_text} {','.join(selected_days_text)}" if selected_days_text else display_text
        if recur_type_value == "monthly":
            day = reminder.get("recurrence_day_of_month", "")
            return f"{display_text}{day}号" if day else display_text
        
        return display_text


    def sort_treeview_column(self, col, reverse):
        try:
            data_list = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
            
            # Define a converter for sorting
            def convert(value):
                if col == "id":
                    try: return int(value)
                    except ValueError: return value # Fallback for non-int IDs if any
                if col == "datetime": # Attempt to parse datetime for proper sorting
                    try: return datetime.datetime.strptime(value, "%Y-%m-%d %H:%M")
                    except ValueError: return value # Fallback if parsing fails
                return str(value).lower() # Default to case-insensitive string sort

            data_list.sort(key=lambda t: convert(t[0]), reverse=reverse)
            for index, (val, k) in enumerate(data_list):
                self.tree.move(k, '', index)
            self.tree.heading(col, command=lambda: self.sort_treeview_column(col, not reverse))
        except Exception as e:
            print(f"Error sorting treeview column {col}: {e}")


    def add_reminder(self):
        title = self.reminder_text.get().strip()
        content = self.reminder_content.get("1.0", tk.END).strip()
        start_date_obj = self.date_entry.get_date()
        time_str = self.time_entry.get().strip()
        use_sound = self.sound_var.get()
        
        recurrence_type_display = self.recurrence_type_var.get() # Get Chinese display
        # Convert to internal English value
        recurrence_type = self.recurrence_type_display_to_value.get(recurrence_type_display, "once")


        if not title:
            if self.root.winfo_exists(): messagebox.showerror("错误", "请输入提醒事项", parent=self.root)
            return
        if not re.match(r'^\d{2}:\d{2}$', time_str):
            if self.root.winfo_exists(): messagebox.showerror("错误", "时间格式不正确，应为 HH:MM", parent=self.root)
            return
        
        try:
            time_obj = datetime.datetime.strptime(time_str, "%H:%M").time()
            # This is the user's desired start datetime for the event series or single event
            user_desired_start_dt = datetime.datetime.combine(start_date_obj, time_obj)

            next_id = (max(r["id"] for r in self.reminders if isinstance(r.get("id"), int)) + 1) if self.reminders else 1

            reminder_definition = {
                "id": next_id,
                "title": title,
                "content": content,
                "status": "待提醒",
                "use_sound": use_sound,
                "recurrence_type": recurrence_type, # Store internal English value
                "target_time_str": time_str, # Store the HH:MM part for future calculations
                "recurrence_days_of_week": None,
                "recurrence_day_of_month": None,
            }

            if recurrence_type == "weekly":
                selected_days = [i for i, var in enumerate(self.weekly_days_vars) if var.get()]
                if not selected_days:
                    if self.root.winfo_exists(): messagebox.showerror("错误", "每周重复至少需要选择一天。", parent=self.root)
                    return
                reminder_definition["recurrence_days_of_week"] = selected_days
            elif recurrence_type == "monthly":
                try:
                    day_of_month = int(self.monthly_day_spinbox.get())
                    if not (1 <= day_of_month <= 31):
                        raise ValueError("日期必须在1-31之间")
                    reminder_definition["recurrence_day_of_month"] = day_of_month
                except ValueError as ve:
                    if self.root.winfo_exists(): messagebox.showerror("错误", f"每月重复的日期无效: {ve}", parent=self.root)
                    return
            
            # Base datetime for calculating the *first* occurrence.
            # If user_desired_start_dt is in the future, calculate from just before it.
            # If user_desired_start_dt is in the past (e.g. setting up a daily reminder for 9am when it's 10am),
            # calculate from "now" to find the *next* valid slot.
            calc_base_dt = min(user_desired_start_dt, datetime.datetime.now()) - datetime.timedelta(microseconds=1)
            if user_desired_start_dt > datetime.datetime.now(): # If desired start is in future
                 calc_base_dt = user_desired_start_dt - datetime.timedelta(microseconds=1)


            # Calculate the first actual occurrence based on rules and desired start time
            first_occurrence_dt = self.calculate_next_occurrence(reminder_definition, calc_base_dt)

            if not first_occurrence_dt:
                if self.root.winfo_exists(): messagebox.showerror("错误", "无法计算首次提醒时间，请检查重复规则和日期。", parent=self.root)
                return

            reminder_definition["datetime"] = first_occurrence_dt.strftime("%Y-%m-%d %H:%M")
            
            # For "once" type, warn if the calculated first occurrence is already past (within a minute tolerance)
            if recurrence_type == "once" and first_occurrence_dt < datetime.datetime.now() - datetime.timedelta(minutes=1):
                 if self.root.winfo_exists() and not messagebox.askyesno("时间警告", "提醒时间似乎已过去，仍要添加吗？", parent=self.root):
                    return

            self.reminders.append(reminder_definition)
            self.save_reminders()
            self.refresh_reminders()

            if self.root.winfo_exists(): messagebox.showinfo("成功", f"提醒已添加:\n{title}\n首次提醒: {reminder_definition['datetime']}", parent=self.root)

        except ValueError as e: # e.g. strptime format error, int conversion
            if self.root.winfo_exists(): messagebox.showerror("错误", f"输入格式不正确: {str(e)}", parent=self.root)
        except Exception as e_gen: # Generic catch-all
            if self.root.winfo_exists(): messagebox.showerror("错误", f"添加提醒时发生错误: {str(e_gen)}", parent=self.root)


    def calculate_next_occurrence(self, reminder_def, strictly_after_this_dt):
        recur_type = reminder_def.get("recurrence_type", "once")
        target_time_str = reminder_def.get("target_time_str") # HH:MM string

        if not target_time_str: return None # Should not happen if target_time_str is always set
        try:
            target_time_obj = datetime.datetime.strptime(target_time_str, "%H:%M").time()
        except ValueError:
            return None # Invalid HH:MM format

        if recur_type == "once":
            # For "once", if it has a pre-defined datetime (e.g. from loaded data or edit),
            # that datetime is the target. We just check if it's after strictly_after_this_dt.
            if 'datetime' in reminder_def: # This key holds the specific YYYY-MM-DD HH:MM for a 'once' event
                potential_dt = datetime.datetime.strptime(reminder_def['datetime'], "%Y-%m-%d %H:%M")
                return potential_dt if potential_dt > strictly_after_this_dt else None
            else: 
                # This case is for a *new* "once" reminder being added via UI.
                # The `start_date_obj` and `time_str` from UI form the basis.
                # `strictly_after_this_dt` here would be (user_desired_start_dt - 1us) or (now - 1us).
                # The intended datetime is effectively (strictly_after_this_dt.date() + target_time_obj)
                # or rather, it's better to use the date from strictly_after_this_dt.
                # The `user_desired_start_dt` (passed implicitly via strictly_after_this_dt construction in add_reminder)
                # already contains the correct date and time.
                # This part is a bit tricky. When ADDING a "once" reminder, the date_entry and time_entry define the *exact* datetime.
                # So, the first_occurrence_dt calculation in add_reminder should effectively just use this.
                # Let's refine `add_reminder` for "once" slightly for clarity.
                # For calculate_next_occurrence, if "once", use the date part of strictly_after_this_dt (or one day later if time passed)
                
                # The `user_desired_start_dt` from `add_reminder` logic IS the date/time for "once".
                # This `calculate_next_occurrence` is called with `calc_base_dt` which is `user_desired_start_dt - 1us`
                # So, the logic should be:
                potential_dt = datetime.datetime.combine(
                    (strictly_after_this_dt + datetime.timedelta(microseconds=1)).date(), # Date part of intended 'once' time
                    target_time_obj
                )
                return potential_dt if potential_dt > strictly_after_this_dt else None

        # For recurring types, iterate from the day after strictly_after_this_dt's date part
        # or from strictly_after_this_dt's date part if the time allows
        start_check_date = strictly_after_this_dt.date()

        for i in range(366 * 2): # Check up to 2 years ahead
            candidate_date = start_check_date + datetime.timedelta(days=i)
            potential_next_dt = datetime.datetime.combine(candidate_date, target_time_obj)

            # Critical: Only consider datetimes *strictly after* the reference point
            if potential_next_dt <= strictly_after_this_dt: 
                continue 

            valid_for_rule = False
            if recur_type == "daily":
                valid_for_rule = True
            elif recur_type == "workday":
                valid_for_rule = self.workday_checker.is_workday(candidate_date)
            elif recur_type == "weekend":
                valid_for_rule = not self.workday_checker.is_workday(candidate_date)
            elif recur_type == "weekly":
                days_of_week = reminder_def.get("recurrence_days_of_week", []) # these are 0-6 indices (Mon-Sun)
                if candidate_date.weekday() in days_of_week:
                    valid_for_rule = True
            elif recur_type == "monthly":
                day_of_month_setting = reminder_def.get("recurrence_day_of_month")
                if day_of_month_setting: # Should be an int
                    _, max_days_in_month = calendar.monthrange(candidate_date.year, candidate_date.month)
                    actual_target_day = min(day_of_month_setting, max_days_in_month)
                    if candidate_date.day == actual_target_day:
                        valid_for_rule = True
            
            if valid_for_rule:
                # We already ensured potential_next_dt > strictly_after_this_dt
                return potential_next_dt
        
        return None # No valid occurrence found within the search range


    def delete_selected(self):
        selected_items = self.tree.selection()
        if not selected_items:
            if self.root.winfo_exists(): messagebox.showwarning("提示", "请先选择一个或多个提醒进行删除", parent=self.root)
            return

        if self.root.winfo_exists() and not messagebox.askyesno("确认删除", f"确定要删除选中的 {len(selected_items)} 个提醒吗?", parent=self.root):
            return

        ids_to_delete = [self.tree.item(item, "values")[0] for item in selected_items]
        self.reminders = [r for r in self.reminders if str(r.get("id","")) not in ids_to_delete]
        self.save_reminders()
        self.refresh_reminders()

    def mark_as_completed(self):
        selected_items = self.tree.selection()
        if not selected_items:
            if self.root.winfo_exists(): messagebox.showwarning("提示", "请先选择一个提醒进行操作", parent=self.root)
            return

        updated_count = 0
        for item_iid in selected_items:
            item_id_str = self.tree.item(item_iid, "values")[0]
            for reminder in self.reminders:
                if str(reminder.get("id","")) == item_id_str:
                    if reminder.get("status") == "已完成": # Already fully completed
                        continue

                    recurrence_type = reminder.get("recurrence_type", "once")
                    if recurrence_type != "once":
                        # For recurring, calculate next occurrence from its current datetime
                        try:
                            current_reminder_dt = datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M")
                        except (ValueError, KeyError):
                            reminder["status"] = "错误 (日期格式)" # Mark as error if current datetime is bad
                            updated_count +=1
                            continue

                        next_dt = self.calculate_next_occurrence(reminder, current_reminder_dt)
                        if next_dt:
                            reminder["datetime"] = next_dt.strftime("%Y-%m-%d %H:%M")
                            reminder["status"] = "待提醒" # Reset for next occurrence
                            updated_count += 1
                        else:
                            # No next occurrence, so it's effectively completed
                            reminder["status"] = "已完成 (无法重复)"
                            updated_count += 1
                    else: # For "once" type, just mark as completed
                        reminder["status"] = "已完成"
                        updated_count += 1
                    break # Move to next selected item
        
        if updated_count > 0:
            self.save_reminders()
            self.refresh_reminders()
            if self.root.winfo_exists(): messagebox.showinfo("成功", f"{updated_count} 个提醒已处理。", parent=self.root)
        else:
            if self.root.winfo_exists(): messagebox.showinfo("提示", "选中的提醒可能已经是完成状态或未找到。", parent=self.root)


    def on_item_double_click(self, event):
        selected_item_iid = self.tree.selection()
        if not selected_item_iid: return
        item_id_str = self.tree.item(selected_item_iid[0], "values")[0]
        self.edit_selected_from_detail(item_id_str)


    def edit_selected_from_detail(self, item_id_to_edit_str):
        # Ensure the item is selected in the tree before opening edit dialog
        found_in_tree = False
        for item_iid in self.tree.get_children(''):
            if self.tree.item(item_iid, "values")[0] == item_id_to_edit_str:
                if item_iid not in self.tree.selection(): # If not already selected (e.g. from popup action)
                    self.tree.selection_set(item_iid) # Select it
                self.tree.focus(item_iid) # Also focus it
                found_in_tree = True
                break
        
        if found_in_tree:
            self.edit_selected() # Now call the main edit function
        else: # Should not happen if called from a valid context
            if self.root.winfo_exists(): messagebox.showerror("错误", f"无法在列表中找到ID为 {item_id_to_edit_str} 的提醒进行编辑。", parent=self.root)


    def refresh_reminders(self):
        # Check if root or tree exists before proceeding
        if not self.root.winfo_exists() or not hasattr(self, 'tree') or not self.tree.winfo_exists(): return
        
        # Preserve selection, focus, and scroll position
        selection = self.tree.selection()
        focused_item = self.tree.focus()
        # yview() returns a tuple (first, last) representing the fraction of the content visible
        scroll_pos_y_tuple = self.tree.yview() 
        
        for item in self.tree.get_children(): self.tree.delete(item)
        
        # Sort reminders primarily by datetime, then by ID for consistent ordering
        sorted_reminders = sorted(self.reminders, key=lambda r: (r.get("datetime", ""), r.get("id", 0)))
        
        for reminder in sorted_reminders:
            recurrence_display = self.get_recurrence_display_text(reminder)
            self.tree.insert("", tk.END, values=(
                reminder.get("id", ""), 
                reminder.get("title", ""), 
                reminder.get("content", ""),
                reminder.get("datetime", ""), 
                recurrence_display,
                reminder.get("status", "")
            ), iid=str(reminder.get("id",""))) # Use ID as IID for easy lookup

        # Restore selection if items still exist
        if selection:
            valid_selection = [s for s in selection if self.tree.exists(s)]
            if valid_selection: self.tree.selection_set(valid_selection)
        # Restore focus if item still exists
        if focused_item and self.tree.exists(focused_item):
            self.tree.focus(focused_item)
            self.tree.see(focused_item) # Ensure focused item is visible

        # Restore scroll position
        if scroll_pos_y_tuple: self.tree.yview_moveto(scroll_pos_y_tuple[0])


    def load_reminders(self):
        if not os.path.exists(self.data_file): self.reminders = []; return
        try:
            with open(self.data_file, "r", encoding="utf-8") as f: loaded_data = json.load(f)
            
            migrated_reminders, max_id, seen_ids, needs_resave = [], 0, set(), False
            
            for i, r_data in enumerate(loaded_data): # Iterate through loaded reminders
                if not isinstance(r_data, dict): # Skip if not a dictionary
                    needs_resave = True; continue

                # ID migration and uniqueness check
                original_id = r_data.get("id"); current_id = original_id
                if not isinstance(current_id, int) or current_id <= 0 or current_id in seen_ids:
                    max_id +=1; current_id = max_id; needs_resave = True
                else: # Valid existing ID
                    if current_id > max_id: max_id = current_id
                seen_ids.add(current_id)

                # target_time_str migration (stores HH:MM for recurrence)
                default_target_time = r_data.get("target_time_str")
                if not default_target_time and "datetime" in r_data: # If missing, try to derive from datetime
                    try:
                        default_target_time = datetime.datetime.strptime(r_data["datetime"], "%Y-%m-%d %H:%M").strftime("%H:%M")
                        needs_resave = True
                    except: pass # Ignore if datetime is invalid
                if not default_target_time: # Fallback if still missing
                    default_target_time = "09:00" # Arbitrary default
                    needs_resave = True
                
                # Construct the reminder dictionary with defaults
                reminder = {
                    "id": current_id, 
                    "title": r_data.get("title", r_data.get("text", "无标题")).strip(), # Migrate "text" to "title"
                    "content": r_data.get("content", "").strip(), 
                    "datetime": r_data.get("datetime", (datetime.datetime.now() + datetime.timedelta(days=1)).strftime("%Y-%m-%d %H:%M")),
                    "status": r_data.get("status", "待提醒"), 
                    "use_sound": r_data.get("use_sound", True),
                    "recurrence_type": r_data.get("recurrence_type", "once"),
                    "target_time_str": default_target_time,
                    "recurrence_days_of_week": r_data.get("recurrence_days_of_week"), # List of ints (0-6) or None
                    "recurrence_day_of_month": r_data.get("recurrence_day_of_month") # Int (1-31) or None
                }
                if "text" in r_data and "title" not in r_data: needs_resave = True # Mark for resave if migration happened
                if "target_time_str" not in r_data : needs_resave = True # Mark for resave if target_time_str was added

                # Validate and fix datetime field if necessary
                try: 
                    datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M")
                except ValueError: # If datetime is invalid
                    needs_resave = True
                    try:
                        # Try to recalculate based on its rules, from a point just before now
                        reset_dt_base = datetime.datetime.now() - datetime.timedelta(microseconds=1)
                        new_valid_dt = self.calculate_next_occurrence(reminder, reset_dt_base)
                        if new_valid_dt:
                            reminder["datetime"] = new_valid_dt.strftime("%Y-%m-%d %H:%M")
                        else: # Fallback if calculation fails
                            reminder["datetime"] = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(f"%Y-%m-%d {reminder['target_time_str']}")
                    except: # Ultimate fallback
                         reminder["datetime"] = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(f"%Y-%m-%d {reminder['target_time_str']}")
                
                migrated_reminders.append(reminder)
            
            self.reminders = migrated_reminders
            if needs_resave: # Save if any migrations or fixes occurred
                self.save_reminders()

        except json.JSONDecodeError as e:
            if self.root.winfo_exists(): messagebox.showerror("加载错误", f"{self.data_file} 格式错误: {str(e)}", parent=self.root)
            self.reminders = []
            # Backup corrupted file
            if os.path.exists(self.data_file):
                try: os.rename(self.data_file, f"{self.data_file}.corrupted_{int(time.time())}")
                except OSError as oe: print(f"Could not backup corrupted file: {oe}")
        except Exception as e:
            if self.root.winfo_exists(): messagebox.showerror("加载错误", f"加载提醒时发生未知错误: {type(e).__name__} - {str(e)}", parent=self.root)
            self.reminders = []


    def save_reminders(self):
        try:
            # Sort before saving for consistency, though refresh_reminders also sorts for display
            self.reminders.sort(key=lambda r: (r.get("datetime", ""), r.get("id", 0)))
            with open(self.data_file, "w", encoding="utf-8") as f:
                json.dump(self.reminders, f, ensure_ascii=False, indent=2)
        except Exception as e: # Catch generic exceptions during file write
            if self.root.winfo_exists(): messagebox.showerror("保存错误", f"无法保存提醒数据: {e}", parent=self.root)


    def check_reminders(self):
        while not self.stop_thread:
            current_time = datetime.datetime.now()
            reminders_to_show_later, needs_save_and_refresh = [], False
            
            # Iterate over a copy of the list if modifications are made directly
            # or use indices if removing items. Here, we modify items in-place.
            for reminder in list(self.reminders): # Iterate a copy if items might be removed, but here we modify
                try:
                    # Basic validation for essential fields
                    if not reminder.get("datetime") or not isinstance(reminder.get("status"), str): 
                        continue # Skip malformed reminders
                    
                    reminder_dt_obj = datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M")
                    
                    if reminder_dt_obj <= current_time and reminder.get("status") == "待提醒":
                        # Ensure we're modifying the actual reminder in self.reminders, not a copy
                        live_reminder_ref = next((r for r in self.reminders if r.get("id") == reminder.get("id")), None)
                        if not live_reminder_ref: continue # Should not happen if list(self.reminders) is used carefully

                        reminders_to_show_later.append(live_reminder_ref.copy()) # Store a copy for the popup

                        # Update status and calculate next occurrence for recurring reminders
                        recurrence_type = live_reminder_ref.get("recurrence_type", "once")
                        if recurrence_type != "once":
                            next_occurrence_dt = self.calculate_next_occurrence(live_reminder_ref, reminder_dt_obj)
                            if next_occurrence_dt:
                                live_reminder_ref["datetime"] = next_occurrence_dt.strftime("%Y-%m-%d %H:%M")
                                # Status remains "待提醒" for the next one
                            else: # No next occurrence
                                live_reminder_ref["status"] = "已完成 (无法重复)"
                        else: # For "once" type
                            live_reminder_ref["status"] = "已提醒" # Or "已完成" if preferred after one pop-up
                        
                        needs_save_and_refresh = True
                
                except ValueError: # Error parsing reminder's datetime string
                    if reminder.get("status") != "错误 (日期格式)": # Avoid repeated updates
                        live_reminder_ref = next((r for r in self.reminders if r.get("id") == reminder.get("id")), None)
                        if live_reminder_ref:
                            live_reminder_ref["status"] = "错误 (日期格式)"
                            needs_save_and_refresh = True
                except Exception as e: # Catch other errors during check
                    print(f"Error checking reminder ID {reminder.get('id','N/A')}: {e}")
            
            if needs_save_and_refresh:
                self.save_reminders()
                if self.root.winfo_exists(): self.root.after(0, self.refresh_reminders)
            
            # Show popups after processing all reminders for this cycle
            for r_to_show in reminders_to_show_later:
                if self.root.winfo_exists(): self.root.after(0, lambda r_copy=r_to_show: self.show_reminder_popup(r_copy))
            
            # Sleep for a short duration before next check
            for _ in range(15): # Check self.stop_thread more frequently for faster shutdown
                if self.stop_thread: break
                time.sleep(1) # Total sleep time of ~15 seconds
            if self.stop_thread: break # Exit loop if stopped
        
    def show_reminder_popup(self, reminder_data):
        # Ensure this runs on the main thread
        if threading.current_thread() != threading.main_thread():
            if self.root.winfo_exists(): # Check if root window still exists
                self.root.after(0, lambda: self.show_reminder_popup(reminder_data))
            return
        if not self.root.winfo_exists(): return # Don't show if main window is gone

        # If main window is minimized/hidden, show it first
        if self.root.state() == 'withdrawn' or self.root.state() == 'iconic':
             self._show_window_thread_safe() # This deiconifies and lifts

        # Play sound
        if reminder_data.get("use_sound", True):
            try:
                # Try to play custom sound if it exists
                sound_file_path = TodoReminderApp.resource_path("dingdingding.wav")
                if os.path.exists(sound_file_path):
                    winsound.PlaySound(sound_file_path, winsound.SND_FILENAME | winsound.SND_ASYNC)
                else:
                    # Fallback to system notification sound
                    winsound.PlaySound("SystemNotification", winsound.SND_ALIAS | winsound.SND_ASYNC)
            except Exception as e:
                # Ultimate fallback if even SystemNotification fails
                try: winsound.PlaySound("SystemHand", winsound.SND_ALIAS | winsound.SND_ASYNC) # Another common sound
                except: pass # Ignore if sound playing fails

        # Create reminder popup window
        reminder_window = tk.Toplevel(self.root)
        reminder_window.title("⏰ 提醒!")
        reminder_window.geometry("450x380") # Adjusted size
        reminder_window.resizable(False, False)
        reminder_window.attributes('-topmost', True)
        reminder_window.focus_force() # Bring to front and focus

        main_popup_frame = ttk.Frame(reminder_window, padding="15")
        main_popup_frame.pack(fill=tk.BOTH, expand=True)
        
        title_label = ttk.Label(main_popup_frame, text=f"{reminder_data.get('title', '无标题')}",
                                font=("TkDefaultFont", 14, "bold"), wraplength=400)
        title_label.pack(pady=(0,10), fill=tk.X)

        # Content area with scrollbar
        content_area_frame = ttk.Frame(main_popup_frame)
        content_area_frame.pack(fill=tk.BOTH, expand=True, pady=(0,10))
        content_scrollbar = ttk.Scrollbar(content_area_frame, orient=tk.VERTICAL)
        content_text_widget = tk.Text(content_area_frame, wrap=tk.WORD, height=6, # Start with a reasonable height
                                      yscrollcommand=content_scrollbar.set, relief=tk.FLAT,
                                      font=self.default_font, borderwidth=0, highlightthickness=0) # No border for cleaner look
        content_scrollbar.config(command=content_text_widget.yview)
        content_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        content_text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        full_content_str = f"内容：\n{reminder_data.get('content', '(无详细内容)')}\n\n时间：{reminder_data.get('datetime', '未知时间')}"
        recur_display_text = self.get_recurrence_display_text(reminder_data)
        if reminder_data.get("recurrence_type", "once") != "once":
            full_content_str += f"\n重复规则：{recur_display_text}"

        content_text_widget.insert("1.0", full_content_str)
        content_text_widget.config(state=tk.DISABLED) # Make read-only
        
        button_frame_popup = ttk.Frame(main_popup_frame)
        button_frame_popup.pack(fill=tk.X, pady=(10,0))

        def mark_complete_and_close_popup(): 
            # Find the reminder in the main list and update its status
            for r_iter in self.reminders:
                if r_iter.get("id") == reminder_data.get("id"):
                    recur_type = r_iter.get("recurrence_type", "once")
                    if recur_type != "once":
                        # 使用当前时间作为计算基准，而不是提醒的原始时间
                        # 这样可以避免当用户延迟处理提醒时，下次提醒日期计算错误的问题
                        current_dt = datetime.datetime.now() - datetime.timedelta(microseconds=1)
                        next_dt = self.calculate_next_occurrence(r_iter, current_dt)
                        if next_dt:
                            r_iter["datetime"] = next_dt.strftime("%Y-%m-%d %H:%M")
                            r_iter["status"] = "待提醒"
                        else:
                            r_iter["status"] = "已完成 (无法重复)"
                    else: # For "once" type
                         r_iter["status"] = "已完成" # More definitive than "已提醒"
                    break
            self.save_reminders(); self.refresh_reminders(); reminder_window.destroy()

        def snooze_and_close(minutes_to_snooze):
            self.snooze_reminder(reminder_data, minutes_to_snooze); reminder_window.destroy()

        btn_options_popup = {"compound": tk.LEFT} # No fixed width, let them expand
        complete_btn_text = "标记完成" if reminder_data.get("recurrence_type", "once") == "once" else "处理此项"
        ttk.Button(button_frame_popup, text=complete_btn_text, style="Success.TButton", command=mark_complete_and_close_popup, **btn_options_popup).pack(side=tk.LEFT, padx=3, expand=True, fill=tk.X)
        
        # Only show snooze if it's not already "completed (no repeat)"
        if reminder_data.get("recurrence_type", "once") == "once" or reminder_data.get("status") != "已完成 (无法重复)":
            ttk.Button(button_frame_popup, text="推迟(5分)", style="Edit.TButton", command=lambda: snooze_and_close(5), **btn_options_popup).pack(side=tk.LEFT, padx=3, expand=True, fill=tk.X)
            ttk.Button(button_frame_popup, text="推迟(10分)", style="Edit.TButton", command=lambda: snooze_and_close(10), **btn_options_popup).pack(side=tk.LEFT, padx=3, expand=True, fill=tk.X)
        
        ttk.Button(button_frame_popup, text="关闭", style="Secondary.TButton", command=reminder_window.destroy, **btn_options_popup).pack(side=tk.LEFT, padx=3, expand=True, fill=tk.X)
        
        # Make popup transient to main window and grab focus
        reminder_window.transient(self.root)
        reminder_window.grab_set()


    def snooze_reminder(self, reminder_data_dict, minutes):
        try:
            reminder_id = reminder_data_dict.get("id")
            parent_win = self.root if self.root.winfo_exists() else None # For messageboxes

            if reminder_id is None: # Should not happen with valid reminder_data
                if parent_win: messagebox.showerror("错误", "无法推迟提醒：缺少提醒ID。", parent=parent_win)
                return
            
            # The `datetime` in `reminder_data_dict` is the time this specific event was triggered.
            current_event_dt_str = reminder_data_dict.get("datetime") 
            if not current_event_dt_str:
                 if parent_win: messagebox.showerror("错误", "无法推迟提醒：缺少原始提醒时间。", parent=parent_win)
                 return

            original_trigger_dt = datetime.datetime.strptime(current_event_dt_str, "%Y-%m-%d %H:%M")
            snoozed_dt = original_trigger_dt + datetime.timedelta(minutes=minutes)

            # Ensure snoozed time is at least 1 minute in the future from now
            min_snooze_from_now = datetime.datetime.now() + datetime.timedelta(minutes=1)
            if snoozed_dt < min_snooze_from_now:
                snoozed_dt = min_snooze_from_now

            # Find and update the reminder in the main list
            found_and_updated = False
            for r_iter in self.reminders:
                if r_iter.get("id") == reminder_id:
                    # For a snoozed reminder, its next effective "datetime" is the snoozed time.
                    # Its status should revert to "待提醒".
                    # The recurrence logic will pick up from this new "datetime" if it's a recurring task
                    # that was snoozed instead of "completed for this cycle".
                    r_iter["datetime"] = snoozed_dt.strftime("%Y-%m-%d %H:%M")
                    r_iter["status"] = "待提醒" # Make it active again for the snoozed time
                    found_and_updated = True; break
            
            if found_and_updated:
                self.save_reminders(); self.refresh_reminders()
            else: # Should not happen if reminder_data_dict was valid
                if parent_win: messagebox.showwarning("未找到", f"未找到ID为 {reminder_id} 的提醒进行推迟。", parent=parent_win)
        except ValueError: # strptime error
            if parent_win: messagebox.showerror("错误", "推迟提醒时日期时间格式错误。", parent=parent_win)
        except Exception as e: # Generic error
            if parent_win: messagebox.showerror("错误", f"推迟提醒失败: {e}", parent=parent_win)


    def edit_selected(self):
        selected_item_iids = self.tree.selection()
        if not selected_item_iids: # Ensure one item is selected
            if self.root.winfo_exists(): messagebox.showwarning("提示", "请先选择一个提醒进行编辑", parent=self.root)
            return
        item_id_str = self.tree.item(selected_item_iids[0], "values")[0] # Get ID from first selected
        
        original_reminder = next((r for r in self.reminders if str(r.get("id","")) == item_id_str), None)
        if not original_reminder: # Should not happen if tree is synced with self.reminders
            if self.root.winfo_exists(): messagebox.showerror("错误", "找不到选中的提醒数据进行编辑。", parent=self.root)
            return

        # Create Edit Window
        edit_window = tk.Toplevel(self.root); edit_window.title(f"编辑提醒 (ID: {item_id_str})")
        edit_window.geometry("520x480"); edit_window.resizable(False, False); edit_window.attributes('-topmost', True)
        edit_window.transient(self.root); edit_window.grab_set() # Modal behavior
        
        edit_main_frame = ttk.Frame(edit_window, padding="15"); edit_main_frame.pack(fill=tk.BOTH, expand=True)
        edit_main_frame.grid_columnconfigure(1, weight=1) # Allow entry fields to expand
        
        row_idx = 0
        # Title
        ttk.Label(edit_main_frame, text="提醒事项:").grid(row=row_idx, column=0, sticky=tk.W, pady=3)
        edit_title_var = tk.StringVar(value=original_reminder.get("title", ""))
        edit_title_entry = ttk.Entry(edit_main_frame, textvariable=edit_title_var, width=45)
        edit_title_entry.grid(row=row_idx, column=1, columnspan=2, sticky=tk.EW, pady=3); row_idx += 1
        
        # Content
        ttk.Label(edit_main_frame, text="提醒内容:").grid(row=row_idx, column=0, sticky=tk.NW, pady=3) # NW for multiline
        edit_content_text = tk.Text(edit_main_frame, width=45, height=4, wrap=tk.WORD, relief=tk.SOLID, borderwidth=1)
        edit_content_text.insert("1.0", original_reminder.get("content", ""))
        edit_content_text.grid(row=row_idx, column=1, columnspan=2, sticky=tk.EW, pady=3); row_idx += 1

        # Date and Time (using current 'datetime' for date, and 'target_time_str' for time)
        try:
            # The 'datetime' field is the *next scheduled occurrence*
            current_event_dt = datetime.datetime.strptime(original_reminder.get("datetime", ""), "%Y-%m-%d %H:%M")
            edit_date_obj = current_event_dt.date() # Use date of next occurrence as default for DateEntry
            # 'target_time_str' is the consistent time for recurring events (e.g., "09:00")
            edit_time_str = original_reminder.get("target_time_str", "09:00") # Fallback if missing
        except ValueError: # If datetime is invalid
            edit_date_obj = datetime.datetime.now().date() # Fallback to today
            edit_time_str = original_reminder.get("target_time_str", "09:00") # Fallback if missing

        edit_datetime_outer_frame = ttk.Frame(edit_main_frame)
        edit_datetime_outer_frame.grid(row=row_idx, column=0, columnspan=3, sticky=tk.EW, pady=3)
        row_idx += 1

        ttk.Label(edit_datetime_outer_frame, text="下次日期:").grid(row=0, column=0, sticky=tk.W, padx=(0,5))
        edit_date_entry = DateEntry(edit_datetime_outer_frame, width=12, date_pattern='yyyy-mm-dd', firstweekday='monday')
        edit_date_entry.set_date(edit_date_obj)
        edit_date_entry.grid(row=0, column=1, sticky=tk.W, padx=5)
        
        ttk.Label(edit_datetime_outer_frame, text="提醒时间:").grid(row=0, column=2, sticky=tk.W, padx=(10,5))
        edit_time_var = tk.StringVar(value=edit_time_str)
        edit_time_entry = ttk.Entry(edit_datetime_outer_frame, textvariable=edit_time_var, width=8)
        edit_time_entry.grid(row=0, column=3, sticky=tk.W, padx=5)

        # Recurrence Type
        ttk.Label(edit_main_frame, text="重复类型:").grid(row=row_idx, column=0, sticky=tk.W, pady=3)
        
        original_recur_type_value = original_reminder.get("recurrence_type", "once") # Internal English value
        original_recur_type_display = self.recurrence_type_map.get(original_recur_type_value, self.recurrence_type_map["once"]) # Chinese display
        
        edit_recur_type_var = tk.StringVar(value=original_recur_type_display) # Use Chinese display for Combobox
        
        recur_display_values_edit = list(self.recurrence_type_map.values()) # All Chinese display values

        edit_recur_combo = ttk.Combobox(edit_main_frame, textvariable=edit_recur_type_var,
                                         values=recur_display_values_edit, state="readonly", width=15)
        edit_recur_combo.grid(row=row_idx, column=1, sticky=tk.W, pady=3)

        edit_recur_options_frame = ttk.Frame(edit_main_frame, style="Recurrence.TFrame")
        edit_recur_options_frame.grid(row=row_idx, column=2, sticky=tk.EW, pady=0, padx=5)
        row_idx += 1

        # Recurrence Options (Weekly/Monthly) - initially hidden, shown by _toggle_edit_recur_options
        edit_weekly_frame = ttk.Frame(edit_recur_options_frame, style="Recurrence.TFrame")
        edit_weekly_vars = [tk.BooleanVar(value=False) for _ in range(7)]
        if original_reminder.get("recurrence_type") == "weekly" and original_reminder.get("recurrence_days_of_week"):
            for day_idx in original_reminder["recurrence_days_of_week"]: # These are 0-6
                if 0 <= day_idx < 7: edit_weekly_vars[day_idx].set(True)
        days_names = ["一", "二", "三", "四", "五", "六", "日"]
        for i, day_name in enumerate(days_names):
            cb = ttk.Checkbutton(edit_weekly_frame, text=day_name, variable=edit_weekly_vars[i], width=3)
            cb.pack(side=tk.LEFT, padx=1)

        edit_monthly_frame = ttk.Frame(edit_recur_options_frame, style="Recurrence.TFrame")
        ttk.Label(edit_monthly_frame, text="每月:").pack(side=tk.LEFT, padx=(0,2))
        edit_monthly_day_var = tk.StringVar(value=str(original_reminder.get("recurrence_day_of_month", "1")))
        edit_monthly_spinbox = ttk.Spinbox(edit_monthly_frame, from_=1, to=31, textvariable=edit_monthly_day_var, width=3, wrap=True)
        edit_monthly_spinbox.pack(side=tk.LEFT)
        ttk.Label(edit_monthly_frame, text="号").pack(side=tk.LEFT, padx=(2,0))

        def _toggle_edit_recur_options(event=None): # Bound to combobox change
            recur_type_display_val = edit_recur_type_var.get() # Chinese display
            recur_type_internal_val = self.recurrence_type_display_to_value.get(recur_type_display_val, "once") # English internal

            edit_weekly_frame.pack_forget(); edit_monthly_frame.pack_forget()
            if recur_type_internal_val == "weekly": 
                edit_weekly_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
            elif recur_type_internal_val == "monthly": 
                edit_monthly_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        edit_recur_combo.bind("<<ComboboxSelected>>", _toggle_edit_recur_options)
        _toggle_edit_recur_options() # Call once to set initial state

        # Sound and Status
        sound_status_frame = ttk.Frame(edit_main_frame)
        sound_status_frame.grid(row=row_idx, column=0, columnspan=3, sticky=tk.EW, pady=3); row_idx += 1
        
        edit_sound_var = tk.BooleanVar(value=original_reminder.get("use_sound", True))
        edit_sound_check = ttk.Checkbutton(sound_status_frame, text="声音提醒", variable=edit_sound_var)
        edit_sound_check.pack(side=tk.LEFT, padx=(0, 20))
        
        ttk.Label(sound_status_frame, text="状态:").pack(side=tk.LEFT, padx=(0,5))
        status_options = ["待提醒", "已提醒", "已完成", "错误 (日期格式)", "已完成 (无法重复)"] # Possible statuses
        edit_status_var = tk.StringVar(value=original_reminder.get("status", "待提醒"))
        edit_status_menu = ttk.OptionMenu(sound_status_frame, edit_status_var, edit_status_var.get(), *status_options)
        edit_status_menu.config(width=15) # Give it some width
        edit_status_menu.pack(side=tk.LEFT, padx=5)


        def save_edited_changes():
            try:
                new_title = edit_title_var.get().strip()
                if not new_title: messagebox.showerror("错误", "请输入提醒事项", parent=edit_window); return
                
                new_content = edit_content_text.get("1.0", tk.END).strip()
                new_target_time_str = edit_time_var.get().strip() # This is HH:MM
                if not re.match(r'^\d{2}:\d{2}$', new_target_time_str): 
                    messagebox.showerror("错误", "提醒时间格式应为 HH:MM", parent=edit_window); return
                
                new_start_date_obj = edit_date_entry.get_date() # This is YYYY-MM-DD from DateEntry
                
                updated_reminder_def = original_reminder.copy() # Start with a copy of the original
                
                recur_type_display_save = edit_recur_type_var.get() # Chinese display
                recurrence_type_internal_save = self.recurrence_type_display_to_value.get(recur_type_display_save, "once") # English internal

                updated_reminder_def.update({
                    "title": new_title, 
                    "content": new_content,
                    "use_sound": edit_sound_var.get(), 
                    "status": edit_status_var.get(), # User can manually set status
                    "recurrence_type": recurrence_type_internal_save, # Save internal English value
                    "target_time_str": new_target_time_str, # Save the HH:MM part
                    "recurrence_days_of_week": None, # Reset these, then fill if applicable
                    "recurrence_day_of_month": None
                })

                # Populate recurrence specifics
                if updated_reminder_def["recurrence_type"] == "weekly":
                    sel_days = [i for i, var in enumerate(edit_weekly_vars) if var.get()]
                    if not sel_days: messagebox.showerror("错误", "每周重复至少需要选择一天。", parent=edit_window); return
                    updated_reminder_def["recurrence_days_of_week"] = sel_days
                elif updated_reminder_def["recurrence_type"] == "monthly":
                    try:
                        day_m = int(edit_monthly_day_var.get())
                        if not (1 <= day_m <= 31): raise ValueError("日期必须在1-31之间")
                        updated_reminder_def["recurrence_day_of_month"] = day_m
                    except ValueError as ve:
                        messagebox.showerror("错误", f"每月重复的日期无效: {ve}", parent=edit_window); return
                
                # --- Datetime recalculation logic ---
                # This is the datetime the user *wants* for the next event or the 'once' event.
                new_desired_dt_for_calc = datetime.datetime.combine(new_start_date_obj, 
                                                                    datetime.datetime.strptime(new_target_time_str, "%H:%M").time())
                
                recalculate_datetime = True
                # If status is manually set to a "final" state, don't auto-recalculate next occurrence.
                # User takes control of the datetime field in this case.
                if updated_reminder_def["status"] not in ["待提醒", "已提醒"]: # e.g. "已完成", "错误"
                    recalculate_datetime = False
                
                if recalculate_datetime:
                    # Base for calculation: just before the new desired datetime.
                    # Or if desired time is past and status is "待提醒", calculate from now.
                    calc_base_dt = new_desired_dt_for_calc - datetime.timedelta(microseconds=1)
                    if updated_reminder_def["status"] == "待提醒" and new_desired_dt_for_calc < datetime.datetime.now():
                         calc_base_dt = datetime.datetime.now() - datetime.timedelta(microseconds=1)

                    next_calculated_dt = self.calculate_next_occurrence(updated_reminder_def, calc_base_dt)
                    
                    if next_calculated_dt:
                        updated_reminder_def["datetime"] = next_calculated_dt.strftime("%Y-%m-%d %H:%M")
                    elif updated_reminder_def["recurrence_type"] != "once": # Recurring but no next date found
                        messagebox.showwarning("计算警告", "无法根据新规则计算下次提醒时间。可能需要调整日期或规则。提醒时间将设为您指定的日期和时间，但状态可能需要调整。", parent=edit_window)
                        updated_reminder_def["datetime"] = new_desired_dt_for_calc.strftime("%Y-%m-%d %H:%M") # Use user's date/time
                        if updated_reminder_def["status"] == "待提醒": updated_reminder_def["status"] = "错误 (规则冲突)" # Or similar
                    else: # "once" type, and it might be in the past based on new_desired_dt_for_calc
                        updated_reminder_def["datetime"] = new_desired_dt_for_calc.strftime("%Y-%m-%d %H:%M")
                else: # Not recalculating, so use the date/time explicitly set by user in edit form.
                     updated_reminder_def["datetime"] = new_desired_dt_for_calc.strftime("%Y-%m-%d %H:%M")

                # Update the reminder in the main list
                updated_in_list = False
                for r_idx, r_item in enumerate(self.reminders):
                    if str(r_item.get("id","")) == item_id_str:
                        self.reminders[r_idx] = updated_reminder_def 
                        updated_in_list = True; break
                
                if updated_in_list: 
                    self.save_reminders(); self.refresh_reminders(); edit_window.destroy()
                    if self.root.winfo_exists(): messagebox.showinfo("成功", "提醒已更新", parent=self.root)
                else: # Should not happen
                    if self.root.winfo_exists(): messagebox.showerror("错误", "更新提醒失败 (未在列表中找到)", parent=edit_window)
            
            except ValueError as e_val: # From strptime, int conversion etc.
                if self.root.winfo_exists(): messagebox.showerror("错误", f"日期或时间格式不正确: {str(e_val)}", parent=edit_window)
            except Exception as e_gen: # Generic catch-all
                if self.root.winfo_exists(): messagebox.showerror("错误", f"保存时发生错误: {str(e_gen)}", parent=edit_window)
        
        # Save/Cancel Buttons
        buttons_edit_frame = ttk.Frame(edit_main_frame)
        buttons_edit_frame.grid(row=row_idx, column=0, columnspan=3, pady=(15,0), sticky=tk.E) # Align to East
        ttk.Button(buttons_edit_frame, text="保存", style="Success.TButton", command=save_edited_changes).pack(side=tk.LEFT, padx=5)
        ttk.Button(buttons_edit_frame, text="取消", style="Secondary.TButton", command=edit_window.destroy).pack(side=tk.LEFT, padx=5)
        
        edit_window.protocol("WM_DELETE_WINDOW", edit_window.destroy) # Handle window close button


    def on_analyze(self):
        text_to_analyze = self.ai_text.get("1.0", tk.END).strip()
        if not text_to_analyze: 
            if self.root.winfo_exists(): messagebox.showwarning("警告", "请输入需要分析的文本", parent=self.root)
            return
        
        # Disable analyze button during processing
        analyze_button_widget = next((c for c in self.ai_text.master.winfo_children() if isinstance(c, ttk.Button) and c.cget('text') == "分析"), None)
        if analyze_button_widget: analyze_button_widget.config(state=tk.DISABLED)
        if self.root.winfo_exists(): self.root.config(cursor="wait"); self.root.update_idletasks()

        def analysis_task():
            try:
                result = self.analyze_text(text_to_analyze)
                if result and self.root.winfo_exists(): # Check root existence before UI updates
                    self.reminder_text.delete(0, tk.END); self.reminder_text.insert(0, result.get("title", ""))
                    self.reminder_content.delete("1.0", tk.END); self.reminder_content.insert("1.0", result.get("content", ""))
                    
                    datetime_str = result.get("datetime", "") # Expected format: YYYY-MM-DD HH:MM
                    parsed_dt = self.parse_flexible_datetime(datetime_str) # Use flexible parser
                    
                    if parsed_dt:
                        self.date_entry.set_date(parsed_dt.date())
                        self.time_entry.delete(0, tk.END); self.time_entry.insert(0, parsed_dt.strftime("%H:%M"))
                    else:
                        if self.root.winfo_exists(): messagebox.showwarning("AI 日期解析警告", "AI未能提供可解析的日期时间，请手动设置。", parent=self.root)
                    
                    if self.root.winfo_exists(): messagebox.showinfo("成功", "AI分析完成，请检查并设置重复规则（如果需要）。", parent=self.root)
            except Exception as e: # Catch any error from analyze_text or UI update
                if self.root.winfo_exists(): messagebox.showerror("AI 分析错误", f"分析过程错误: {e}", parent=self.root)
            finally: # Always re-enable button and reset cursor
                if self.root.winfo_exists():
                    self.root.config(cursor="")
                    if analyze_button_widget and analyze_button_widget.winfo_exists(): analyze_button_widget.config(state=tk.NORMAL)
                    self.root.update_idletasks()
        threading.Thread(target=analysis_task, daemon=True).start()


    def parse_flexible_datetime(self, datetime_str_original):
        if not datetime_str_original: return None
        
        datetime_str, now = str(datetime_str_original), datetime.datetime.now()
        
        # Normalizations
        datetime_str = datetime_str.replace("：", ":").replace("号", "日")
        datetime_str = datetime_str.replace("点半", ":30").replace("点", ":00") # 2点 -> 2:00
        # Correct "2:0030分" to "2:30" if "点半" was already processed and then "分" is re-added by mistake
        datetime_str = re.sub(r"(\d{1,2}):00(\d{1,2})分?", r"\1:\2", datetime_str) 
        datetime_str = datetime_str.replace(" ", "") # Remove all spaces

        # Try standard formats first
        fmts_with_year = ["%Y-%m-%d%H:%M", "%Y/%m/%d%H:%M", "%Y年%m月%d日%H:%M"]
        for fmt in fmts_with_year:
            try: return datetime.datetime.strptime(datetime_str, fmt)
            except ValueError: pass
        
        # Fallback to more complex parsing if year is missing or relative dates
        year, month, day = now.year, now.month, now.day # Default to today
        hour, minute = -1, -1 # Sentinel for "not found yet"

        # Attempt to parse date part (month, day, optional year) and time part
        # Regex tries to find M/D or Y/M/D and optional H:M
        # Example: "2024年5月20日14:30", "5月20日 14:30", "5月20日"
        m = re.search(r"(?:(\d{4})[年/-])?(\d{1,2})[月/-](\d{1,2})日?(?:\s*(?:上午|下午)?\s*(\d{1,2}):(\d{1,2}))?", datetime_str)
        if m:
            parsed_year_str, parsed_month_str, parsed_day_str, parsed_hour_str, parsed_minute_str = m.groups()
            
            if parsed_month_str and parsed_day_str:
                month, day = int(parsed_month_str), int(parsed_day_str)
                if parsed_year_str: 
                    year = int(parsed_year_str)
                else: # No year given, intelligent guess
                    # If M/D is before today's M/D, assume next year
                    if datetime.date(now.year, month, day) < now.date():
                        year = now.year + 1
                    else: # Otherwise, assume current year
                        year = now.year
            
            if parsed_hour_str and parsed_minute_str:
                hour, minute = int(parsed_hour_str), int(parsed_minute_str)
        else: # If no date part found, try to find just time
            m_time = re.search(r"(\d{1,2}):(\d{1,2})", datetime_str)
            if m_time:
                hour, minute = int(m_time.group(1)), int(m_time.group(2))

        # Handle relative dates like "明天", "后天", "今天"
        # These override the date parsed above if present, but keep time if parsed.
        if "明天" in datetime_str_original or "明日" in datetime_str_original :
            dt_offset = now + datetime.timedelta(days=1); year, month, day = dt_offset.year, dt_offset.month, dt_offset.day
        elif "后天" in datetime_str_original or "後日" in datetime_str_original:
            dt_offset = now + datetime.timedelta(days=2); year, month, day = dt_offset.year, dt_offset.month, dt_offset.day
        elif "今天" in datetime_str_original or "今日" in datetime_str_original: # Explicit "today"
            year, month, day = now.year, now.month, now.day
        
        # If time was not found by regex, default to current time's H:M or a sensible default like 09:00
        if hour == -1 or minute == -1 : # If time part is still missing
             hour, minute = now.hour, now.minute # Fallback to current time of day as last resort

        try:
            # Validate hour and minute ranges before constructing datetime
            if not (0 <= hour <= 23 and 0 <= minute <= 59):
                 raise ValueError("Invalid hour or minute parsed from string.")
            
            parsed_dt_candidate = datetime.datetime(year, month, day, hour, minute)
            
            # If only time was found (no explicit date or relative date),
            # and the resulting datetime is in the past for today, assume next day's time.
            if not m and (m_time or (hour != -1 and minute != -1)) and parsed_dt_candidate < now: 
                 parsed_dt_candidate += datetime.timedelta(days=1) # e.g., "10:00" when it's 11:00 means 10:00 tomorrow
            
            return parsed_dt_candidate
        except ValueError as e: # Catches errors from datetime constructor (e.g., Feb 30) or custom raises
            print(f"Datetime construction error for '{datetime_str_original}' (normalized to '{datetime_str}'): {e}"); return None


    def monitor_clipboard(self):
        last_clipboard_content_local = ""
        try: # Initial paste to set baseline, add guard for truly empty initial clipboard
            last_clipboard_content_local = pyperclip.paste() + "__INIT_GUARD__" 
        except pyperclip.PyperclipException as e:
            print(f"Clipboard access error on init: {e}. Clipboard monitoring disabled.")
            return # Disable if clipboard is not accessible
        
        while not self.stop_thread:
            try:
                # Remove null bytes which can cause issues, then strip whitespace
                current_content = pyperclip.paste().replace('\x00', '').strip() 
                if current_content and current_content != last_clipboard_content_local:
                    if self.should_analyze_clipboard(current_content):
                        # Schedule GUI update from main thread
                        if self.root.winfo_exists(): self.root.after(0, lambda c=current_content: self.handle_clipboard_content(c))
                    last_clipboard_content_local = current_content
                
                # Sleep briefly to avoid hammering CPU and pyperclip
                for _ in range(30): # ~1.5s (30 * 0.05s), allows faster stop_thread check
                    if self.stop_thread: return
                    time.sleep(0.05) 
            except pyperclip.PyperclipException as e: # Handle errors during paste
                print(f"Clipboard access error: {e}. Retrying...")
                time.sleep(5) # Wait longer if there's an issue
            except Exception as e: # Catch other unexpected errors
                print(f"Unexpected error in clipboard monitor: {e}")
                time.sleep(5)


    def should_analyze_clipboard(self, content_to_check):
        # Basic length checks to avoid processing tiny or huge strings
        if not content_to_check or len(content_to_check) < 5 or len(content_to_check) > 300: return False 
        
        content_lower = content_to_check.lower() # Case-insensitive keyword check
        # Remove spaces for keyword check to catch "待 办" etc.
        content_no_space = "".join(content_lower.split())
        
        # 检查是否包含关键词
        has_keyword = any(k.lower() in content_no_space for k in self.keywords)
        
        # 如果没有关键词，但文本较短且包含时间表达，也可能是提醒
        # 例如："明天下午三点半到公司开会"
        is_short_with_time = len(content_to_check) < 50 and any(word in content_lower for word in ["明天", "今天", "后天", "下周", "下月"])
        
        # 检查是否包含日期时间模式
        has_datetime_pattern = any(re.search(p, content_to_check, re.IGNORECASE) for p in self.datetime_patterns)
        
        # 对于短文本，更宽松地判断是否包含时间表达式
        if not has_datetime_pattern and len(content_to_check) < 50:
            # 检查是否包含时间表达式，如"三点"、"下午三点半"等
            time_words = ["点", "分", "小时", "半", "刻", "上午", "中午", "下午", "晚上"]
            has_time_words = any(word in content_lower for word in time_words)
            
            # 检查是否包含数字（可能表示时间）
            has_numbers = bool(re.search(r'[0-9一二三四五六七八九十]', content_lower))
            
            # 如果同时包含时间词和数字，认为可能是时间表达式
            has_datetime_pattern = has_time_words and has_numbers
        
        # 返回结果：有关键词且有时间模式，或者是短文本且包含时间表达式
        return (has_keyword and has_datetime_pattern) or is_short_with_time


    def handle_clipboard_content(self, content_from_clipboard):
        if not self.root.winfo_exists(): return # Abort if GUI is gone
        
        # Debounce: If same content was auto-filled recently, don't ask again immediately
        if content_from_clipboard == getattr(self, '_last_clipboard_auto_filled', None) and \
           hasattr(self, '_last_clipboard_analyzed_time') and \
           (time.time() - self._last_clipboard_analyzed_time < 10): # 10-second debounce
            return

        self._show_window_thread_safe() # Bring window to front
        if self.root.winfo_exists(): # Re-check after potential de-iconify
             # Delay slightly to ensure window is fully visible before messagebox
             self.root.after(150, lambda cfc=content_from_clipboard: self._ask_analyze_clipboard(cfc))


    def _ask_analyze_clipboard(self, content_from_clipboard):
        if not self.root.winfo_exists(): return # Final check

        # Truncate and clean content for display in messagebox
        display_content = content_from_clipboard[:120].strip().replace(chr(10), ' ') # Show first 120 chars, newlines as spaces
        if len(content_from_clipboard) > 120: display_content += "..."
        
        if messagebox.askyesno("剪贴板内容识别",
                               f"剪贴板中似乎有待办事项信息:\n\n'{display_content}'\n\n是否使用AI分析并填充表单？",
                               parent=self.root): # Ensure messagebox is parented
            self.ai_text.delete("1.0", tk.END); self.ai_text.insert("1.0", content_from_clipboard)
            self.last_clipboard_content = content_from_clipboard # Update general last_clipboard_content
            
            # For debouncing this specific auto-fill action
            self._last_clipboard_auto_filled = content_from_clipboard
            self._last_clipboard_analyzed_time = time.time()
            
            self.on_analyze() # Trigger AI analysis
        else:
            # User said no, but still update last_clipboard_content to avoid re-triggering for this exact content
            self.last_clipboard_content = content_from_clipboard


def main():
    root = tk.Tk()
    start_minimized = "--startup" in sys.argv
    app = TodoReminderApp(root, start_minimized=start_minimized) 
    root.mainloop()
    
if __name__ == "__main__":
    # If running as a PyInstaller bundle, change CWD to the extracted temp folder
    if hasattr(sys, '_MEIPASS'):
        os.chdir(sys._MEIPASS)
    main()